<?
/**
 * Webapp Authentication
 */
(basename($_SERVER['PHP_SELF']) == basename(__FILE__)) && die('Access Denied');

/**
 * Webapp class for defining properties of webapp users
 */
class appAuth {

/**
 * Password requirement defaults
 */
private $passwordMinLength = 6;
private $passwordRequireUpperCase = true;
private $passwordRequireNumber    = true;
private $passwordRequireSpecial   = false;

/**
 * Allowed password characters
 */
private $pcLower   = null;
private $pcUpper   = null;
private $pcNumber  = null;
private $pcSpecial = null;

/**
 * Constructor - bring up session
 */
public function __construct() {
	// Populate acceptable password characters
	$this->pcLower   = str_split('abcdefghijklmnopqrstuvwxyz');
	$this->pcUpper   = str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
	$this->pcNumber  = str_split('0123456789');
	$this->pcSpecial = str_split('~`!@#$%^&*()_-=+[]{}|:;\<,>.?/"\'');

	// apply optional config settings
	if (defined('PASS_MIN_LENGTH') and is_numeric(PASS_MIN_LENGTH)) {
		$this->passwordMinLength = PASS_MIN_LENGTH;
		}
	if (defined('PASS_REQUIRE_UC') and is_bool(PASS_REQUIRE_UC)) {
		$this->passwordRequireUpperCase = PASS_REQUIRE_UC;
		}
	if (defined('PASS_REQUIRE_NUM') and is_bool(PASS_REQUIRE_NUM)) {
		$this->passwordRequireNumber = PASS_REQUIRE_NUM;
		}
	if (defined('PASS_REQUIRE_SC') and is_bool(PASS_REQUIRE_SC)) {
		$this->passwordRequireSpecial = true;
		}
	if (!is_numeric($this->passwordMinLength) or $this->passwordMinLength < 6) {
		$this->passwordMinLength = 6;
		}
	}

/**
 * Authenticate a login request
 * @param string $username
 * @param string $password
 * @param User $dbo
 * @return string error message on failures
 * @return array $userinfo on success
 */
public function authenticate($username = null, $password = null, &$dbo = null) {
	if (!is_string($username)) { return WA_FAIL_NO_USERNAME; }
	if (!is_string($password)) { return WA_FAIL_NO_PASSWORD; }
	if (!($dbo instanceof User)) { return WA_FAIL_OBJECT; }
	// Set up query object
	$dbo->searchReset();
	$dbo->setSearchSelect('DisplayName');
	$dbo->setSearchSelect('FirstName');
	$dbo->setSearchSelect('Id');
	$dbo->setSearchSelect('IsAdmin');
	$dbo->setSearchSelect('Enabled');
	$dbo->setSearchSelect('LastName');
	$dbo->setSearchSelect('Login');
	// filter
	$dbo->setSearchFilter('Login',    $username);
	$dbo->setSearchFilter('Password', $password);
	// limit
	$dbo->setSearchLimit(1);
	$user = $dbo->query();
	if (!is_array($user)) { return WA_FAIL_QUERY; }
	if (!count($user))    { return WA_FAIL_INVALID_CREDS; }
	// Account Disabled
	if ($user[0]['Enabled'] == '0') { return WA_FAIL_DISABLED; }

	// Return user information
	return $user[0];
	}

/**
 * Getters for password character requirements.
 */
public function getPasswordCharsLower() {
	return $this->pcLower;
	}
public function getPasswordCharsNumber() {
	return $this->pcNumber;
	}
public function getPasswordCharsSpecial() {
	return $this->pcSpecial;
	}
public function getPasswordCharsUpper() {
	return $this->pcUpper;
	}
public function getPasswordMinimumLength() {
	return $this->passwordMinLength;
	}
public function getPasswordRequireLower() {
	return true;
	}
public function getPasswordRequireNumber() {
	return $this->passwordRequireNumber;
	}
public function getPasswordRequireSpecial() {
	return $this->passwordRequireSpecial;
	}
public function getPasswordRequireUpper() {
	return $this->passwordRequireUpperCase;
	}

/**
 * Obtain a random temporary password string.
 * @return string
 */
public function getTemporaryPassword() {
	$this->passwordMinLength = $this->passwordMinLength;
	// Get a single upper case
	$uc = $this->pcUpper[array_rand($this->pcUpper, 1)];
	// Get a single lower case
	$lc = $this->pcLower[array_rand($this->pcLower, 1)];
	// Get single special character
	$sc = $this->pcSpecial[array_rand($this->pcSpecial, 1)];
	// Get single number
	$rn = $this->pcNumber[array_rand($this->pcNumber, 1)];
	// Initialize pool the characters will come from, and the actual password
	// characters, with their minimum requirements set.
	$passChars = array($lc);
	if ($this->passwordRequireUpperCase) {
		$passChars[] = $uc;
		}
	if ($this->passwordRequireNumber) {
		$passChars[] = $rn;
		}
	if ($this->passwordRequireSpecial) {
		$passChars[] = $sc;
		}
	// Fill in the remaining characters
	while (count($passChars) < $this->passwordMinLength) {
		srand((double)microtime()*1000000);
		$passChars[] = $this->pcLower[array_rand($this->pcLower, 1)];
		}
	return implode('',$passChars);
	}

/**
 * Process a change password request
 * Requires the current session object, user object, and the desired new
 * password.
 * @param User $uo
 * @param appSession $so
 * @param string $password - new password
 * @return string error message on failures
 * @return bool true on success
 */
public function passwordChange(&$uo = null, &$so = null, $password=null) {
	if (!($uo instanceof User))       { return WA_FAIL_OBJECT; }
	if (!($so instanceof appSession)) { return WA_FAIL_OBJECT; }
	if (!$so->isAuthenticated()) {
		return 'Must authenticate before attempting to change passwords.';
		}

	// check length requirements
	$rc = $this->passwordMeetsLengthRequirements($password);
	if (!($rc === true)) { return $rc; }

	// check uniqueness
	$rc = $this->passwordMeetsCharacterRequirements($password);
	if (!($rc === true)) { return $rc; }

	// Populate user object
	$uid = $so->getUserData('Id');
	if (!is_numeric($uid) or $uid < 1) {
		return 'Failed to determine UID of authenticated user.';
		}
	$uo->resetProperties();
	$rc = $uo->populateFromDb($uid);
	if (!($rc === true)) {
		return 'System Error 2, please contact support.';
		}

	// Spice things up if needed...
	if (defined('PASS_SALT')) {
		$md5pass = md5(PASS_SALT . ':' . $password);
		} else {
		$md5pass = md5($password);
		}
	$pass1 = $uo->getProperty('Password');
	$uo->setProperty('Password', $md5pass);
	$rc = $uo->update();
	if (!($rc === true)) {
		return 'System Error 3, please contact support.';
		}
	return true;
	}

/**
 * Does specified password meet password length requirements?
 * @param string $password
 * @return bool true on success
 * @return string error message on failures
 */
private function passwordMeetsLengthRequirements($password = null) {
	if (empty($password)) { return 'Empty passwords are not allowed.'; }
	if (!is_string($password)) { return 'Empty passwords are not allowed.'; }
	$err = 'The system setting for passwordMinLength is invalid.';
	if (!is_numeric($this->passwordMinLength)) { return $err; }
	if ($this->passwordMinLength < 1) { return $err; }
	if (strlen($password) < $this->passwordMinLength) {
		return 'The password does not meet the minimum length requirements.';
		}
	return true;
	}

/**
 * Does specified password meet the uniqueness requirements?
 * @param string $pass
 * @return bool true on success
 * @return string error message on failures
 */
private function passwordMeetsCharacterRequirements($pass = null) {
	if (!is_string($pass) or strlen($pass) < $this->passwordMinLength) {
		return 'Password must be a string of at least '
		. $this->passwordMinLength . ' characters.';
		}
	$passChars  = str_split($pass);
	$hasLower   = false;
	$hasUpper   = false;
	$hasNumber  = false;
	$hasSpecial = false;
	$acceptableChars = array_merge($this->pcLower, $this->pcUpper,
	                              $this->pcNumber, $this->pcSpecial);
	// Check for acceptable characters and flag character types detected.
	foreach($passChars as &$char) {
		if (!in_array($char, $acceptableChars)) {
			return 'The specified password contains illegal characters.';
			}
		// check for lower case
		if (!$hasLower and in_array($char, $this->pcLower)) {
			$hasLower = true;
			}
		// check for upper case
		if (!$hasUpper and in_array($char, $this->pcUpper)) {
			$hasUpper = true;
			}
		// check for numeric
		if (!$hasNumber and in_array($char, $this->pcNumber)) {
			$hasNumber = true;
			}
		// check special
		if (!$hasSpecial and in_array($char, $this->pcSpecial)) {
			$hasSpecial = true;
			}
		}
	// All passwords require at least one lower case
	if (!$hasLower) {
		return 'Passwords require at least one lower case character.';
		}
	// Upper required?
	if ($this->passwordRequireUpperCase and !$hasUpper) {
		return 'Passwords require at least one upper case character.';
		}
	if ($this->passwordRequireNumber and !$hasNumber) {
		return 'Passwords require at least one numeric character.';
		}
	if ($this->passwordRequireSpecial and !$hasSpecial) {
		return 'Passwords require at least one special character.';
		}
	return true;
	}
}
?>
